﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Web.UI;
using System.Web.UI.WebControls;
using SBToolkit.Core;

namespace SBToolkit.Web
{
    /// <summary>
    /// A class used to bind the values of and object's properties with web <see cref="Control"/>s 
    /// that have their ID property set to the name of the object property.
    /// </summary>
    public static class FormBinding
    {
        /// <summary>
        /// Binds an object's properties to <see cref="Control"/>s with the same ID as the propery name.
        /// </summary>
        /// <param name="obj">The object being bound to forms Controls.</param>
        /// <param name="container">The control in which form Controls reside (usually a Page or ContainerControl).</param>
        public static void BindObjectToControls(object obj, Control container)
        {
            BindObjectToControls(obj, container, String.Empty);
        }

        /// <summary>
        /// Binds an object's properties to <see cref="Control"/>s with the same ID as the propery name.
        /// </summary>
        /// <param name="obj">The object being bound to forms Controls.</param>
        /// <param name="container">The control in which form Controls reside (usually a Page or ContainerControl).</param>
        /// <param name="expressionPartSeparator">The expression part separator.</param>
        public static void BindObjectToControls(object obj, Control container, string expressionPartSeparator)
        {
            if (obj == null)
            {
                return;
            }

            var separator = expressionPartSeparator.ToCharArray();
            var controls = container.GetChildControlCollection<Control>().Where(c => !String.IsNullOrEmpty(c.ID));

            foreach (var control in controls)
            {
                var objectPropertyInfo = GetObjectPropertyInfo(obj, control.ID.Split(separator));

                if (objectPropertyInfo.Value == null)
                {
                    continue;
                }

                // handle ListControls (DropDownList, CheckBoxList, RadioButtonList)
                if (control is ListControl)
                {
                    var listControl = control as ListControl;
                    var listItem = listControl.Items.FindByValue(objectPropertyInfo.Value.ToString());
                    if (listItem != null)
                    {
                        listControl.SelectedIndex = listControl.Items.IndexOf(listItem);
                    }
                }
                else
                {
                    // get the properties of the control
                    var controlProperties = control.GetType().GetProperties();

                    // test for common properties
                    bool success = FindAndSetControlProperty(objectPropertyInfo, control, controlProperties, "Checked", typeof(bool));
                    if (!success)
                    {
                        success = FindAndSetControlProperty(objectPropertyInfo, control, controlProperties, "SelectedDate", typeof(DateTime));
                    }
                    if (!success)
                    {
                        success = FindAndSetControlProperty(objectPropertyInfo, control, controlProperties, "Text", typeof(string));
                    }
                    if (!success)
                    {
                        FindAndSetControlProperty(objectPropertyInfo, control, controlProperties, "Value", typeof(string));
                    }
                }
            }
        }

        /// <summary>
        /// Binds values in <see cref="Control"/>s to a business object properties with the same name as the control ID.
        /// </summary>
        /// <param name="obj">The object being bound to Control values.</param>
        /// <param name="container">The control in which form Controls reside (usually a Page or ContainerControl).</param>
        public static void BindControlsToObject(object obj, Control container)
        {
            BindControlsToObject(obj, container, String.Empty);
        }

        /// <summary>
        /// Binds values in <see cref="Control"/>s to a business object properties with the same name as the control ID.
        /// </summary>
        /// <param name="obj">The object being bound to Control values.</param>
        /// <param name="container">The control in which form Controls reside (usually a Page or ContainerControl).</param>
        /// <param name="expressionPartSeparator">The expression part separator.</param>
        public static void BindControlsToObject(object obj, Control container, string expressionPartSeparator)
        {
            if (obj == null)
            {
                return;
            }

            var separator = expressionPartSeparator.ToCharArray();
            var controls = container.GetChildControlCollection<Control>().Where(c => !String.IsNullOrEmpty(c.ID));

            foreach (var control in controls)
            {
                var objectPropertyInfo = GetObjectPropertyInfo(obj, control.ID.Split(separator));

                if (objectPropertyInfo.PropertyInfo == null)
                {
                    continue;
                }

                if (control is ListControl)
                {
                    var listControl = control as ListControl;
                    if (listControl.SelectedItem != null && !string.IsNullOrEmpty(listControl.SelectedValue))
                    {
                        objectPropertyInfo.Value = listControl.SelectedItem.Value.To(objectPropertyInfo.PropertyInfo.PropertyType);
                    }
                }
                else
                {
                    // get the properties of the control
                    var controlProperties = control.GetType().GetProperties();

                    // test for common properties
                    bool success = FindAndGetControlProperty(objectPropertyInfo, control, controlProperties, "Value", typeof(string));
                    if (!success)
                    {
                        success = FindAndGetControlProperty(objectPropertyInfo, control, controlProperties, "Checked", typeof(bool));
                    }

                    if (!success)
                    {
                        success = FindAndGetControlProperty(objectPropertyInfo, control, controlProperties, "SelectedDate", typeof(DateTime));
                    }

                    if (!success)
                    {
                        FindAndGetControlProperty(objectPropertyInfo, control, controlProperties, "Text", typeof(string));
                    }
                }
            }
        }

        /// <summary>
        /// Binds values in <see cref="Control"/>s to a business object properties with the same name as the control ID.
        /// </summary>
        /// <param name="objects">Objects being bound to Control values.</param>
        /// <param name="containerItems">Containers in which Controls reside (usually a DataItem).</param>
        public static void BindControlsToObjectList<T, K>(IList<T> objects, IList<K> containerItems)
            where T : new()
            where K : Control
        {
            if (objects == null)
            {
                objects = new List<T>(containerItems.Count);
            }

            if (objects.Count == 0)
            {
                for (int i = 0; i < containerItems.Count; i++)
                {
                    objects.Add(new T());
                }
            }

            if (objects.Count == containerItems.Count)
            {
                for (int i = 0; i < containerItems.Count; i++)
                {
                    BindControlsToObject(objects[i], containerItems[i]);
                }
            }
        }

        /// <summary>
        /// Looks for a property name and type on a control and attempts to set it to the value in an object's property of the same name.
        /// </summary>
        /// <param name="obj">Object property info.</param>
        /// <param name="control">The control whose ID matches the object's property name.</param>
        /// <param name="controlProperties">A set of the control's properties.</param>
        /// <param name="propertyName">The name of the Control property being set.</param>
        /// <param name="type">The correct type for the Control property.</param>
        /// <returns><c>True</c> when property was found and set; otherwise, <c>false</c>.</returns>
        private static bool FindAndSetControlProperty(ObjectPropertyInfo obj, Control control, IEnumerable<PropertyInfo> controlProperties, string propertyName, Type type)
        {
            var property = controlProperties.Where(p => p.Name.Equals(propertyName, StringComparison.InvariantCultureIgnoreCase) && p.PropertyType == type).FirstOrDefault();
            if (property != null)
            {
                property.SetValue(control, obj.Value.To(type), null);

                return true;
            }

            return false;
        }

        /// <summary>
        /// Looks for a property name and type on a control and attempts to set it to the value in an object's property of the same name.
        /// </summary>
        /// <param name="obj">Object property info.</param>
        /// <param name="control">The control whose ID matches the object's property name.</param>
        /// <param name="controlProperties">A set of the control's properties.</param>
        /// <param name="propertyName">The name of the Control property being retrieved.</param>
        /// <param name="type">The correct type for the Control property.</param>
        /// <returns><c>True</c> when property was found and retrieved; otherwise, <c>false</c>.</returns>
        private static bool FindAndGetControlProperty(ObjectPropertyInfo obj, Control control, IEnumerable<PropertyInfo> controlProperties, string propertyName, Type type)
        {
            var property = controlProperties.Where(p => p.Name == propertyName && p.PropertyType == type).FirstOrDefault();
            if (property != null)
            {
                // set the control's property to the business object property value
                try
                {
                    obj.Value = property.GetValue(control, null).To(obj.PropertyInfo.PropertyType);

                    return true;
                }
                catch
                {
                    // the data from the form control could not be converted to objProperty.PropertyType
                }
            }

            return false;
        }

        /// <summary>
        /// Gets specified object property info.
        /// </summary>
        /// <param name="obj">The object to get property info.</param>
        /// <param name="expressionParts">The expression parts.</param>
        /// <returns>Object property info.</returns>
        private static ObjectPropertyInfo GetObjectPropertyInfo(object obj, string[] expressionParts)
        {
            object currentObject = obj;
            object parentObject = null;
            PropertyInfo property = null;

            for (int i = 0; i < expressionParts.Length && currentObject != null; i++)
            {
                parentObject = currentObject;

                property = currentObject.GetType().GetProperties().Where(p => p.Name.Equals(expressionParts[i], StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
                if (property != null)
                {
                    currentObject = property.GetValue(currentObject, null);
                }
            }

            return new ObjectPropertyInfo(parentObject, property);
        }

        /// <summary>
        /// Represents <see cref="PropertyInfo" /> with related object instance.
        /// </summary>
        private class ObjectPropertyInfo
        {
            /// <summary>
            /// Current object.
            /// </summary>
            private readonly object obj;

            /// <summary>
            /// Property info.
            /// </summary>
            private readonly PropertyInfo propertyInfo;
            
            /// <summary>
            /// Value for the current object instance.
            /// </summary>
            private object value;

            /// <summary>
            /// Initializes a new instance of the <see cref="ObjectPropertyInfo"/> class.
            /// </summary>
            /// <param name="obj">Current object.</param>
            /// <param name="propertyInfo">Property info.</param>
            public ObjectPropertyInfo(object obj, PropertyInfo propertyInfo)
            {
                this.obj = obj;
                this.propertyInfo = propertyInfo;
            }

            /// <summary>
            /// Gets the <see cref="PropertyInfo"/>.
            /// </summary>
            /// <value>The property info.</value>
            public PropertyInfo PropertyInfo
            {
                get
                {
                    return propertyInfo;
                }
            }

            /// <summary>
            /// Gets or sets the value for the current object instance.
            /// </summary>
            /// <value>The value.</value>
            public object Value
            {
                get
                {
                    if (value == null && PropertyInfo != null)
                    {
                        value = PropertyInfo.GetValue(obj, null);
                    }

                    return value;
                }
                set
                {
                    PropertyInfo.SetValue(obj, value, null);
                }
            }
        }
    }
}